;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; \brief	演示延时与函数，控制LCD灯闪烁的间隔
; \details	使用Keil Simulator模拟器，和Debug时的Logic Analyzer示波器窗口查看输出
; \remark	File format: UTF-8，源文件使用UTF-8中文编码
; \note		省略了8051的初始化，使用模拟器时会自动准备好运行环境
; \author	将狼才鲸
; \date		2024-07-21
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; 8051汇编指令及每条指令所消耗的时钟数详见：
; https://gitee.com/langcai1943/8051-from-boot-to-application/blob/develop/02_doc/01_8051寄存器、指令集、伪指令和关键字介绍.md

; 当前工程中配置的晶振是Keil默认的24MHz，时钟周期41.67ns，状态周期是2倍时钟周期即83.3333ns，
; 机器周期是6倍状态周期即500ns，一个机器周期记为1T，一条指令周期为1~4T，具体哪条指令占多少T需要查阅上面文档

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 声明 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
$NOMOD51	; 不使用Keil默认隐含的8051通用寄存器定义，而是显式的包含寄存器定义头文件

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 头文件 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
$INCLUDE(at89c51xd2.inc)	; 也可以使用 #include <at89c51xd2.inc>，实际文件在C:\Keil_v5\C51\ASM\at89c51xd2.inc

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 宏定义 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
INS_SET_10US_NUM	EQU 20	; 10us占20个机器周期；当前工程中配置的晶振是Keil默认的24MHz，时钟周期41.67ns，机器周期500ns；

;; 宏定义函数
DELAY_1US  MACRO
	NOP	; 1T，500ns
	NOP
ENDM

DELAY_2US  MACRO
	DELAY_1US
	DELAY_1US
ENDM

DELAY_4US  MACRO
	DELAY_2US
	DELAY_2US
ENDM

DELAY_8US  MACRO
	DELAY_4US
	DELAY_4US
ENDM

DELAY_16US  MACRO
	DELAY_8US
	DELAY_8US
ENDM

;;
; \brief	汇编主函数，演示延时与函数，控制LCD灯闪烁的间隔
;			其实不算真正的函数，只是标号 + 死循环
; \param	无
; \return	无
;;
MAIN:	; 标号或函数名
	; 请在Debug后通过 View--Analysis Windows--Logic Analyzer--Setup--点击右上角叉叉左边的新建图标--键入P1
	; --点击选中P1--在Max那里将0xFF改成0x01--在And Mask中将0xFFFFFFFF改成0x00000001--Close 来创建模拟器中示波器的采样引脚；
	; 把示波器的小窗口向下拉一点，露出波形的显示区域；
	; 再点击Zoom中的All来显示整个波形（不点击会看不到变化的波形）；
	; 再F10单步运行，一边运行一边看输出的矩形波。
	CLR		P1.0			; 1T（耗时1个机器周期），P1_0 IO口置低；P1.0里面的.0是汇编的特殊语法，意思是前面P1寄存器的第0 bit，能位寻址的寄存器都能这样调用
	;; 和CLR一起延时10us，再补齐9.5us
	NOP						; 1T，0.5us
	DELAY_8US				; 宏定义函数
	DELAY_1US

	;; 延时990us
	MOV		R7, #99			; 1T；延时99 x 10 = 990us；0xR7 = 十进制99；调用函数时，第一个参数是0xR7
	LCALL	_delay_us_10x	; 2T，传入的参数是R7

	;; 延时4ms
	MOV		R6, #HIGH(4)	; HIGH和LOW是Keil C51的伪指令，用于获取16位立即数中的高字节和低字节
	MOV		R7, #LOW(4)		; 传入的参数0xR6R7 = 4ms
	LCALL	_delay_ms		; 调用函数；和C语言不一样，汇编函数的定义即使在本调用下方的话，不用先声明也能使用

	SETB	P1.0			; 1T，P1_0 IO口拉高
	;; 和SETB、LJMP MAIN一起延时10us，再补齐8.5us
	NOP						; 1T，0.5us
	DELAY_8US				; 宏定义函数

	;; 延时1000us
	MOV		R7, #99			; 0xR7 = 99
	LCALL	_delay_us_10x	; 传入的参数是99，延时990us

	;; 延时9ms
	MOV		R6, #HIGH(9)
	MOV		R7, #LOW(9)		; 传入的参数0xR6R7 = 9ms
	LCALL	_delay_ms

	LJMP	MAIN			; 2T，跳转到标号

;;
; \brief	以10us为单位的延时函数（必须延时20us及以上，否则请直接使用NOP进行延时！）
; \details	1. 使用小写标号（函数名），在C语言中调用该函数时让函数名与C语言标准更统一；
;			2. 传入的参数需要 >= 2，否则会有异常的长时间延时，也就是说至少延时2个10us；
; \note		注意：要使用汇编实现精准延时的话，需要在调用延时前关闭所有中断，调用后再恢复中断！
; \remark	C语言调用此汇编函数时使用 delay_us_10x(100); // 延时1000us
; \param	0xR7	uint8，延时多少个10us，取值为 2 ~ 255
; \return	无
;;
_delay_us_10x:
	; 外部调用此函数时如果有给R7赋值则会额外消耗1T（已优化）
	; 外部调用此函数的LCALL或ACALL会额外占2T（已优化）
	DEC		R7					; 1T，将10us的次数直接减1；用于补齐10us，抵消额外的消耗

	;; 抵消额外消耗的6T，再补14T凑成10us；
	;  \note	如果晶振有变化，则这里的补时也要调整，或者修改得和 INS_SET_10US_NUM 相关
	DELAY_4US					; 这里是宏定义函数
	DELAY_2US
	DELAY_1US

	;; 延时10us
DELAY_US_LOOP2:
	MOV		R6, #((INS_SET_10US_NUM - 4) / 2)	; 1T
	NOP											; 1T，空指令，什么也不做；为了补齐MOV指令的1T时间到2T
DELAY_US_LOOP1:
	DJNZ	R6, DELAY_US_LOOP1	; 2T

	DJNZ	R7, DELAY_US_LOOP2	; 2T，延时多少个10us

	RET	; 从函数中返回；返回额外占2T（已优化）

;;
; \brief	ms延时函数
; \details	1. 传入的参数必须 >= 1，即至少延时1ms，如果为0则会导致超长时间延时
;			2. 当低字节为0x01时，本延时函数少了2T也就是1us；
;			3. 当低字节为0x00时，本延时函数多了4T也就是2us；
; \note		注意：要使用汇编实现精准延时的话，需要在调用延时前关闭所有中断，调用后再恢复中断！
; \remark	C语言调用此汇编函数时使用 delay_ms(1000); // 延时1000ms
; \param	0xR6R7-->0xR4R5	uint16，延时多少ms；取值范围为0x0002~0xFEFF（注意不是0xFFFF!）
; \return	无
;;
_delay_ms:
	; 外部调用此函数时如果有给R6R7赋值则会额外消耗2T（已优化）
	; 外部调用此函数的LCALL或ACALL会额外占2T（已优化）

	; 0xR4R5 = 0xR6R7
	MOV		A, R7	; 1T，因为之后R7还需作为子函数的参数，所以将0xR6R7先移动到0xR4R5
	MOV		R5, A	; 1T，不能直接MOV R5, R7; 没有这种指令，两个内部寄存器之间不能直接赋值
	MOV		A, R6	; 1T，下面调用us延时函数时没有进行压栈弹栈操作，us延时里面用了R6作为临时变量，所以这里也要避开
	MOV		R4, A	; 1T

	INC		R4		; 1T，R4自增1；如果R4本来为0，则本函数最后的R4减1不为0跳转会有bug；因此高位的取值只能是0x00~0xFE，不能取到0xFF

	;; 函数进入和函数返回时的额外消耗是24T（其中有2T是R4自增一导致的），12us，此处将其补齐到1ms，
	;  还需额外补980us + 8us；并后续将0xR4R5减1，也就是减去这补齐的1ms
	DELAY_8US
	MOV		R7, #98			; 1T已抵消，_delay_us_10x函数的参数，980us
	LCALL	_delay_us_10x	; 2T已抵消

	;; 将0xR4R5减1，用于时间凑整补齐
	;  执行所有INC或DEC自增自减、所有ADD加法指令时都不会产生PSW寄存器的CY借位；
	;  只有ADDC和SUBB指令才带进位或借位，进位和借位都是PSW寄存器的CY位；
	;  SUBB不能用R0~R7去减别的数，只能用累加器A去减
	;; 低字节减1
	MOV		A, R5	; 1T
	CLR		C		; 1T，CLR C和CLR CY是一样的效果，C是指令集里面特定的用法，CY是头文件中BIT伪指令定义的位
	SUBB	A, #1	; 1T
	MOV		R5, A	; 1T，减完后放回R5
	JNC		DELAY_MS_HIGH_BYTE_IGNORE_DEC	; 2T，如果CY进位为0则跳转
	;; 类似跳转指令还有：JNZ累加器为1跳转，JZ累加器为0跳转，JNC进位为0跳转，JC进位为1跳转，
	;  JNB比特为0跳转，JB比特为1跳转，JBC比特为1跳转并清零（前面其他指令都不会自动清零）

	;; 如果有借位则高字节减1（因为有减1的步骤，所以传入的参数不能为0x0001）
	;  当低字节为0x00时，此处会执行，则会多了4T也就是2us
	;                               ;=====================;
	CLR		C		; 1T，CLR C和CLR CY是一样的效果，C是指令集里面特定的用法，CY是头文件中BIT伪指令定义的位
	MOV		A, R4	; 1T
	SUBB	A, #1	; 1T，SUBB使用前要清零进位CY，否则借位存在的话会多减去1
	MOV		R4, A	; 1T，减完后放回R4

DELAY_MS_HIGH_BYTE_IGNORE_DEC:

	;; 处理传入的参数低字节为0的情况
	MOV		A, R5					; 1T
	JZ		DELAY_MS_HIGH_BYTE_JMP	; 2T，累加器A为0则跳转
	LJMP	DELAY_MS_BYTE_LOOP_LOW	; 2T，当低字节减完1后为0时，此处会少了2T也就是1us，少的这1us没有进行优化
	;                                                          ;=====================;
DELAY_MS_BYTE_LOOP_HIGH:
	; 每次高字节循环256ms时的额外消耗是4T，2us；此处将其补齐到10us，后面再调用990us
	DELAY_8US

	;; 延时1ms；高字节减1，低字节从256变成255时，将少的那一次补上
	MOV		R7, #99			; 1T已抵消，_delay_us_10x函数的参数
	LCALL	_delay_us_10x	; 2T已抵消

	MOV		R5, #0FFH		; 2T，R5 = 255
	;; 延时1 ~ 255ms
DELAY_MS_BYTE_LOOP_LOW:
	; 每次低字节循环1ms时的额外消耗是2T，1us，此处将其补齐到10us，后面再调用990us
	DELAY_8US
	DELAY_1US

	;; 延时1ms
	MOV		R7, #99			; 1T已抵消，_delay_us_10x函数的参数
	LCALL	_delay_us_10x	; 2T已抵消

	DJNZ	R5, DELAY_MS_BYTE_LOOP_LOW	; 2T，R5寄存器内的数据减1，不为0则跳转；延时多少个ms

DELAY_MS_HIGH_BYTE_JMP:
	DJNZ	R4, DELAY_MS_BYTE_LOOP_HIGH	; 2T，处理高字节，延时多少个256ms；之前有加1，函数调用时这里额外多了2T（已优化）

	RET	; 2T，返回额外占2T（已优化）

END	; 源文件结束
